namespace Legend.Core.Tests.Caching.PurgedInMemoryCacheTests
{
    using System;
    using FakeItEasy;
    using Legend.Caching;
    using Legend.Tests;
    using NUnit.Framework;

    [TestFixture]
    public abstract class CacheTests
    {
        TimeSpan timeToLive;

        [SetUp]
        public void SetUp()
        {
            this.OnSetUp();
        }

        protected virtual void OnSetUp()
        {
            timeToLive = TimeSpan.FromMinutes(1);
        }

        protected abstract ICache CreateCache();

        [Test]
        public void GetValue_should_be_null_guarded()
        {
            NullGuardedConstraint.Assert(() => Cache.GetValue(A.Fake<ICache>(), "foo", timeToLive, () => "produced"));
        }

        [Test]
        public void GetValue_should_return_value_from_cache_when_it_exists_there()
        {
            // Arrange
            var cache = this.CreateCache();
            cache.Insert("foo", "bar", timeToLive);

            // Act
            var result = cache.GetValue("foo", timeToLive, () => "produced");

            // Assert
            Assert.That(result, Is.EqualTo("bar"));
        }

        [Test]
        public void GetValue_should_return_value_produced_by_value_producer_when_key_does_not_exist_in_cache()
        {
            // Arrange
            var cache = this.CreateCache();

            // Act
            var result = cache.GetValue("foo", timeToLive, () => "produced");

            // Assert
            Assert.That(result, Is.EqualTo("produced"));
        }

        [Test]
        public void GetValue_should_insert_value_produced_by_value_producer_when_key_does_not_exist_in_cache()
        {
            // Arrange
            var cache = this.CreateCache();

            // Act
            var result = cache.GetValue("foo", timeToLive, () => "produced");

            // Assert
            object output;
            Assert.That(cache.TryGetValue("foo", out output), Is.True);
            Assert.That(output, Is.EqualTo("produced"));
        }

        [Test]
        public void TryGetValue_should_return_false_when_value_does_not_exist_in_cache()
        {
            // Arrange
            var cache = this.CreateCache();

            // Act
            int result;
            var gotValue = cache.TryGetValue("foo", out result);

            // Assert
            Assert.That(gotValue, Is.False);
        }

        [Test]
        public void TryGetValue_should_return_true_when_value_exists()
        {
            // Arrange
            var cache = this.CreateCache();
            cache.Insert("foo", 10, TimeSpan.FromHours(1));

            // Act
            int result;
            var gotValue = cache.TryGetValue("foo", out result);

            // Assert
            Assert.That(gotValue, Is.True);
        }

        [Test]
        public void TryGetValue_should_assign_value_to_output()
        {
            // Arrange
            var cache = this.CreateCache();
            cache.Insert("foo", 10, TimeSpan.FromHours(1));

            // Act
            int result;
            cache.TryGetValue("foo", out result);

            // Assert
            Assert.That(result, Is.EqualTo(10));
        }

        [Test]
        public void TryGetValue_should_return_false_when_value_is_not_of_specified_type()
        {
            // Arrange
            var cache = this.CreateCache();
            cache.Insert("foo", "foo", TimeSpan.FromHours(1));

            // Act
            int result;
            var gotValue = cache.TryGetValue("foo", out result);
            
            // Assert
            Assert.That(gotValue, Is.False);
        }

        [Test]
        public void TryGetValue_should_return_false_when_value_is_null_in_cache_and_requested_type_is_value_type()
        {
            // Arrange
            var cache = this.CreateCache();
            cache.Insert("foo", null, TimeSpan.FromHours(1));

            // Act
            int result;
            var gotValue = cache.TryGetValue("foo", out result);

            // Assert
            Assert.That(gotValue, Is.False);
        }

        [Test]
        public void TryGetValue_should_return_true_when_value_in_cache_is_null_and_type_is_reference_type()
        {
            // Arrange
            var cache = this.CreateCache();
            cache.Insert("foo", null, TimeSpan.FromHours(1));

            // Act
            string result;
            var gotValue = cache.TryGetValue("foo", out result);

            // Assert
            Assert.That(gotValue, Is.True);
        }

        [Test]
        public void TryGetValue_should_return_true_when_value_in_cache_is_null_and_type_is_nullable()
        {
            // Arrange
            var cache = this.CreateCache();
            cache.Insert("foo", null, TimeSpan.FromHours(1));

            // Act
            int? result;
            var gotValue = cache.TryGetValue("foo", out result);

            // Assert
            Assert.That(gotValue, Is.True);
        }

        [Test]
        public void Insert_should_overwrite_existing_value_when_using_same_key_twice()
        {
            // Arrange
            var cache = this.CreateCache();
            
            // Act
            cache.Insert("key", 1, TimeSpan.FromDays(1));
            cache.Insert("key", 2, TimeSpan.FromDays(1));

            // Assert
            int retrievedValue;
            cache.TryGetValue("key", out retrievedValue);
            Assert.That(retrievedValue, Is.EqualTo(2));
        }
    }
}